package com.phison.fenestrapro.dashboard.widgetset.client;

import java.util.Date;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.user.client.Cookies;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.Timer;
import com.vaadin.terminal.gwt.client.VConsole;
import com.vaadin.terminal.gwt.client.ValueMap;

/**
 *
 * 
 * @author Niall O'Hara
 *
 */
public class ApplicationConnection extends
		com.vaadin.terminal.gwt.client.ApplicationConnection {

	private static final String COOKIE_NAME = "SmartSessionData";

	private static final int ALLOWED_DELTA = 2000;

	private Date lastActivity;

	private static final int IGNORED_EVENTS = Event.ONMOUSEOUT
			| Event.ONMOUSEMOVE | Event.ONMOUSEOVER | Event.ONMOUSEWHEEL
			| Event.ONMOUSEDOWN;

	public ApplicationConnection() {
		Event.addNativePreviewHandler(new NativePreviewHandler() {

			public void onPreviewNativeEvent(NativePreviewEvent event) {
				if ((IGNORED_EVENTS & event.getTypeInt()) == event.getTypeInt()) {
					return;
				}
				VConsole.log("Extending session due"
						+ event.getNativeEvent().getType());
				updateActivityTime();
			}
		});

		/*
		 * Update activity cookie in a loop (not to tease browser on each event
		 * more than what is necessary).
		 */
		new Timer() {
			@Override
			public void run() {
				syncActivityCookie();
			}
		}.scheduleRepeating(1000);
	}

	private void syncActivityCookie() {
		if (lastActivity != null) {
			String cookie = Cookies.getCookie(COOKIE_NAME + "-activity");
			boolean update = false;
			if (cookie == null) {
				update = true;
			} else {
				long cookieTs = Long.parseLong(cookie);
				if (cookieTs == lastActivity.getTime()) {
					return;
				} else if (cookieTs < lastActivity.getTime()) {
					update = true;
				} else {
					lastActivity = new Date(cookieTs);
				}
			}
			if (update) {
				// let other windows know about latest activity
				Cookies.setCookie(COOKIE_NAME + "-activity",
						"" + lastActivity.getTime());
			}
		}

	}

	private Timer warningTimer = new Timer() {

		@Override
		public void run() {
			boolean changedByCookie = readCookie();
			
			if(keepAlive) {
				// force extension if necessary
				lastActivity = new Date();
			}
			syncActivityCookie();
			boolean extended = checkAndRequestSessionExtension();
			if (!changedByCookie && !extended) {
				TimeoutWarning w = getTimeoutWarning();
				w.show(lastKnownTimoutTs);
				warningVisible = true;
			} else {
				VConsole.error("Session was extended by "
						+ (changedByCookie ? "cookie" : "event"));
				scheduleSessionWarning();
			}
		}
	};
	private int sessionTimeoutMs;
	private long lastKnownTimoutTs;
	private String extraParams;
	private TimeoutWarning w;

	private boolean warningVisible;

	private boolean expired;

	private Long clientServerTimeDelta;

	@Override
	protected void makeUidlRequest(String requestData, String extraParams,
			boolean forceSync) {
		if (expired) {
			VConsole.log("Ignored UIDL request, already expired");
			return;
		}
		if (this.extraParams != null) {
			if (extraParams != null && !extraParams.equals("")) {
				extraParams += "&" + this.extraParams;
			} else {
				extraParams = this.extraParams;
			}
		}
		super.makeUidlRequest(requestData, extraParams, forceSync);
		suspendSessionTimoutCounter();
	}

	private TimeoutWarning getTimeoutWarning() {
		if (w == null) {
			w = GWT.create(TimeoutWarning.class);
			w.setApplicationConnection(this);
		}
		return w;
	}

	private void suspendSessionTimoutCounter() {
		warningTimer.cancel();
	}

	boolean deltarequestSent = false;

	private boolean clearScreenOnTimeout;

	private String sessionExpiredUrl;

	private boolean keepAlive;

	@Override
	protected void handleUIDLMessage(Date start, String jsonText, ValueMap json) {
		super.handleUIDLMessage(start, jsonText, json);
		if (json.containsKey(COOKIE_NAME)) {
			String smd = json.getString(COOKIE_NAME);
			Cookies.setCookie(COOKIE_NAME, smd);
			if (json.containsKey(COOKIE_NAME + "-timeout")) {
				sessionTimeoutMs = json.getInt(COOKIE_NAME + "-timeout") * 1000;

				clearScreenOnTimeout = json.containsKey(COOKIE_NAME
						+ "-clearScreen");
				
				keepAlive = json.containsKey(COOKIE_NAME
						+ "-keepAlive");

				if (json.containsKey(COOKIE_NAME + "-expiredUrl")) {
					sessionExpiredUrl = json.getString(COOKIE_NAME
							+ "-expiredUrl");
				} else {
					sessionExpiredUrl = null;
				}

			}
			lastActivity = null;
			readCookie();
			VConsole.log("SmartSession session timeout:"
					+ new Date(lastKnownTimoutTs));
			VConsole.log("SmartSessions:: server-client delta:"
					+ clientServerTimeDelta);
			scheduleSessionWarning();
		}

	}

	public boolean readCookie() {
		String cookie = Cookies.getCookie(COOKIE_NAME);
		if (cookie != null) {
			long newTimeoutTs = Long.parseLong(cookie);
			long delta = 0;
			if (clientServerTimeDelta == null) {
				/*
				 * Note, we will ignore server process and transfer time. To
				 * minimize this, we create a separate request to fine tune
				 * this, we force a dummy server request (most probably very
				 * fast).
				 */
				delta = new Date().getTime()
						- (newTimeoutTs - sessionTimeoutMs);
				if (!deltarequestSent) {
					Scheduler.get().scheduleDeferred(new ScheduledCommand() {
						public void execute() {
							if (clientServerTimeDelta == null
									&& !hasActiveRequest()) {
								sendPendingVariableChanges();
							}
						}
					});
					deltarequestSent = true;
				} else {
					// save for later
					VConsole.log("Saving client server delta " + delta);
					clientServerTimeDelta = delta;
				}
			} else {
				delta = clientServerTimeDelta;
			}
			newTimeoutTs += delta;

			/*
			 * sesionTimeoutMs != sessionRemainingSinceLastAccessMs if last
			 * request was "session extension request".
			 */

			// TODO ignore meaningless changes ??
			// TODO handle possible race condition (action happened during
			// request)
			boolean extendedKnownExpiration = newTimeoutTs > lastKnownTimoutTs;
			if (extendedKnownExpiration) {
				lastKnownTimoutTs = newTimeoutTs;
				VConsole.log("This or another window has hit server, session will expire at "
						+ new Date(newTimeoutTs));
				if (warningVisible) {
					getTimeoutWarning().close();
					warningVisible = false;
				}
			} else {
				VConsole.log("No changes in session expiration time");
			}
			return extendedKnownExpiration;
		}
		return false;
	}

	private void updateActivityTime() {
		lastActivity = new Date();
		if (warningVisible) {
			checkAndRequestSessionExtension();
			getTimeoutWarning().close();
			warningVisible = false;
		}
	}

	private boolean checkAndRequestSessionExtension() {
		syncActivityCookie();
		if (lastActivity != null) {
			/*
			 * If known expiration time is smaller than via activity ts, extend
			 * session.
			 */
			if (lastKnownTimoutTs < lastActivity.getTime() + sessionTimeoutMs
					- ALLOWED_DELTA) {
				// / TODO only react on significant deltas to last communication
				// ts
				// TODO fix with possible (significant) server-client delta
				/*
				 * The session should extend on all client side activity with
				 * sessionTiemout (exceptions? mouseover?). Send the newly
				 * calculated timeout to server where servlet will update the
				 * session. So if we update text field 5 min after last server
				 * visit, the session will only extend 5 minutes, not with the
				 * whole session timeout.
				 */
				long time = lastActivity.getTime() + sessionTimeoutMs
						- clientServerTimeDelta;
				VConsole.log("Extending session to (server time) "
						+ new Date(time));
				extraParams = "EXTEND=" + time;
				sendPendingVariableChanges();

				return true;
			}
		}
		return false;
	}

	public void scheduleSessionWarning() {
		warningTimer.cancel();
		warningVisible = false;

		//int msBeforeActualTimeout = (int) (sessionTimeoutMs - sessionTimeoutMs * getWarningFraction());
		
		int msBeforeActualTimeout = (int) (sessionTimeoutMs - (sessionTimeoutMs - (1000 * 120)));

		long ts = lastKnownTimoutTs - msBeforeActualTimeout;
		int warningTimeout = (int) (ts - new Date().getTime());
		if (warningTimeout < 1) {
			warningTimeout = 1;
		}
		VConsole.log("Timeout for warning set to:" + (warningTimeout / 1000)
				+ " seconds. Check or warning at "
				+ new Date(new Date().getTime() + warningTimeout));
		warningTimer.schedule(warningTimeout);

	}

	public void forceExpire() {
		// TODO if in "app" mode, kill app only
		// makeUidlRequest("", "INVALIDATE=1", false);
		expired = true;
	}

	public String getSessionExpiredUrl() {
		return sessionExpiredUrl;
	}

	public boolean clearScreenOnTimemout() {
		return clearScreenOnTimeout;
	}

}